<!-- Licensed under a BSD license. See license.html for license -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>Three.js - Indexed Textures - Random Colors</title>
    <style>
    html, body {
        height: 100%;
        margin: 0;
        font-family: sans-serif;
    }
    #c {
        width: 100%;  /* let our container decide our size */
        height: 100%;
        display: block;
    }
    #container {
      position: relative;  /* makes this the origin of its children */
      width: 100%;
      height: 100%;
      overflow: hidden;
    }
    #labels {
      position: absolute;  /* let us position ourself inside the container */
      z-index: 0;          /* make a new stacking context so children don't sort with rest of page */
      left: 0;             /* make our position the top left of the container */
      top: 0;
      color: white;
    }
    #labels>div {
      position: absolute;  /* let us position them inside the container */
      left: 0;             /* make their default position the top left of the container */
      top: 0;
      cursor: pointer;     /* change the cursor to a hand when over us */
      font-size: small;
      user-select: none;   /* don't let the text get selected */
      pointer-events: none;  /* make us invisible to the pointer */
      text-shadow:         /* create a black outline */
        -1px -1px 0 #000,
         0   -1px 0 #000,
         1px -1px 0 #000,
         1px  0   0 #000,
         1px  1px 0 #000,
         0    1px 0 #000,
        -1px  1px 0 #000,
        -1px  0   0 #000;
    }
    #labels>div:hover {
      color: red;
    }
    </style>
  </head>
  <body>
    <div id="container">
      <canvas id="c"></canvas>
      <div id="labels"></div>
    </div>
  </body>
<script type="importmap">
{
  "imports": {
    "three": "../../build/three.module.js",
    "three/addons/": "../../examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

function main() {

	const canvas = document.querySelector( '#c' );
	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );

	const fov = 60;
	const aspect = 2; // the canvas default
	const near = 0.1;
	const far = 10;
	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
	camera.position.z = 2.5;

	const controls = new OrbitControls( camera, canvas );
	controls.enableDamping = true;
	controls.enablePan = false;
	controls.minDistance = 1.2;
	controls.maxDistance = 4;
	controls.update();

	const scene = new THREE.Scene();
	scene.background = new THREE.Color( '#246' );

	const pickingScene = new THREE.Scene();
	pickingScene.background = new THREE.Color( 0 );

	const maxNumCountries = 512;
	const paletteTextureWidth = maxNumCountries;
	const paletteTextureHeight = 1;
	const palette = new Uint8Array( paletteTextureWidth * 4 );
	const paletteTexture = new THREE.DataTexture( palette, paletteTextureWidth, paletteTextureHeight );
	paletteTexture.minFilter = THREE.NearestFilter;
	paletteTexture.magFilter = THREE.NearestFilter;
	paletteTexture.colorSpace = THREE.SRGBColorSpace;
	for ( let i = 1; i < palette.length; ++ i ) {

		palette[ i ] = Math.random() * 256;

	}

	// set the ocean color (index #0)
	palette.set( [ 100, 200, 255, 255 ], 0 );
	paletteTexture.needsUpdate = true;

	{

		const loader = new THREE.TextureLoader();
		const geometry = new THREE.SphereGeometry( 1, 64, 32 );

		const indexTexture = loader.load( 'resources/data/world/country-index-texture.png', render );
		indexTexture.minFilter = THREE.NearestFilter;
		indexTexture.magFilter = THREE.NearestFilter;

		const pickingMaterial = new THREE.MeshBasicMaterial( { map: indexTexture } );
		pickingScene.add( new THREE.Mesh( geometry, pickingMaterial ) );

		const fragmentShaderReplacements = [
			{
				from: '#include <common>',
				to: `
          #include <common>
          uniform sampler2D indexTexture;
          uniform sampler2D paletteTexture;
          uniform float paletteTextureWidth;
        `,
			},
			{
				from: '#include <color_fragment>',
				to: `
          #include <color_fragment>
          {
            vec4 indexColor = texture2D(indexTexture, vMapUv);
            float index = indexColor.r * 255.0 + indexColor.g * 255.0 * 256.0;
            vec2 paletteUV = vec2((index + 0.5) / paletteTextureWidth, 0.5);
            vec4 paletteColor = texture2D(paletteTexture, paletteUV);
            // diffuseColor.rgb += paletteColor.rgb;   // white outlines
            diffuseColor.rgb = paletteColor.rgb - diffuseColor.rgb;  // black outlines
          }
        `,
			},
		];

		const texture = loader.load( 'resources/data/world/country-outlines-4k.png', render );
		const material = new THREE.MeshBasicMaterial( { map: texture } );
		material.onBeforeCompile = function ( shader ) {

			fragmentShaderReplacements.forEach( ( rep ) => {

				shader.fragmentShader = shader.fragmentShader.replace( rep.from, rep.to );

			} );
			shader.uniforms.paletteTexture = { value: paletteTexture };
			shader.uniforms.indexTexture = { value: indexTexture };
			shader.uniforms.paletteTextureWidth = { value: paletteTextureWidth };

		};

		scene.add( new THREE.Mesh( geometry, material ) );

	}

	async function loadJSON( url ) {

		const req = await fetch( url );
		return req.json();

	}

	let numCountriesSelected = 0;
	let countryInfos;
	async function loadCountryData() {

		countryInfos = await loadJSON( 'resources/data/world/country-info.json' ); /* threejs.org: url */

		const lonFudge = Math.PI * 1.5;
		const latFudge = Math.PI;
		// these helpers will make it easy to position the boxes
		// We can rotate the lon helper on its Y axis to the longitude
		const lonHelper = new THREE.Object3D();
		// We rotate the latHelper on its X axis to the latitude
		const latHelper = new THREE.Object3D();
		lonHelper.add( latHelper );
		// The position helper moves the object to the edge of the sphere
		const positionHelper = new THREE.Object3D();
		positionHelper.position.z = 1;
		latHelper.add( positionHelper );

		const labelParentElem = document.querySelector( '#labels' );
		for ( const countryInfo of countryInfos ) {

			const { lat, lon, min, max, name } = countryInfo;

			// adjust the helpers to point to the latitude and longitude
			lonHelper.rotation.y = THREE.MathUtils.degToRad( lon ) + lonFudge;
			latHelper.rotation.x = THREE.MathUtils.degToRad( lat ) + latFudge;

			// get the position of the lat/lon
			positionHelper.updateWorldMatrix( true, false );
			const position = new THREE.Vector3();
			positionHelper.getWorldPosition( position );
			countryInfo.position = position;

			// compute the area for each country
			const width = max[ 0 ] - min[ 0 ];
			const height = max[ 1 ] - min[ 1 ];
			const area = width * height;
			countryInfo.area = area;

			// add an element for each country
			const elem = document.createElement( 'div' );
			elem.textContent = name;
			labelParentElem.appendChild( elem );
			countryInfo.elem = elem;

		}

		requestRenderIfNotRequested();

	}

	loadCountryData();

	const tempV = new THREE.Vector3();
	const cameraToPoint = new THREE.Vector3();
	const cameraPosition = new THREE.Vector3();
	const normalMatrix = new THREE.Matrix3();

	const settings = {
		minArea: 20,
		maxVisibleDot: - 0.2,
	};

	function updateLabels() {

		// exit if we have not loaded the data yet
		if ( ! countryInfos ) {

			return;

		}

		const large = settings.minArea * settings.minArea;
		// get a matrix that represents a relative orientation of the camera
		normalMatrix.getNormalMatrix( camera.matrixWorldInverse );
		// get the camera's position
		camera.getWorldPosition( cameraPosition );
		for ( const countryInfo of countryInfos ) {

			const { position, elem, area, selected } = countryInfo;
			const largeEnough = area >= large;
			const show = selected || ( numCountriesSelected === 0 && largeEnough );
			if ( ! show ) {

				elem.style.display = 'none';
				continue;

			}

			// Orient the position based on the camera's orientation.
			// Since the sphere is at the origin and the sphere is a unit sphere
			// this gives us a camera relative direction vector for the position.
			tempV.copy( position );
			tempV.applyMatrix3( normalMatrix );

			// compute the direction to this position from the camera
			cameraToPoint.copy( position );
			cameraToPoint.applyMatrix4( camera.matrixWorldInverse ).normalize();

			// get the dot product of camera relative direction to this position
			// on the globe with the direction from the camera to that point.
			// -1 = facing directly towards the camera
			// 0 = exactly on tangent of the sphere from the camera
			// > 0 = facing away
			const dot = tempV.dot( cameraToPoint );

			// if the orientation is not facing us hide it.
			if ( dot > settings.maxVisibleDot ) {

				elem.style.display = 'none';
				continue;

			}

			// restore the element to its default display style
			elem.style.display = '';

			// get the normalized screen coordinate of that position
			// x and y will be in the -1 to +1 range with x = -1 being
			// on the left and y = -1 being on the bottom
			tempV.copy( position );
			tempV.project( camera );

			// convert the normalized position to CSS coordinates
			const x = ( tempV.x * .5 + .5 ) * canvas.clientWidth;
			const y = ( tempV.y * - .5 + .5 ) * canvas.clientHeight;

			// move the elem to that position
			elem.style.transform = `translate(-50%, -50%) translate(${x}px,${y}px)`;

			// set the zIndex for sorting
			elem.style.zIndex = ( - tempV.z * .5 + .5 ) * 100000 | 0;

		}

	}

	class GPUPickHelper {

		constructor() {

			// create a 1x1 pixel render target
			this.pickingTexture = new THREE.WebGLRenderTarget( 1, 1 );
			this.pixelBuffer = new Uint8Array( 4 );

		}
		pick( cssPosition, scene, camera ) {

			const { pickingTexture, pixelBuffer } = this;

			// set the view offset to represent just a single pixel under the mouse
			const pixelRatio = renderer.getPixelRatio();
			camera.setViewOffset(
				renderer.getContext().drawingBufferWidth, // full width
				renderer.getContext().drawingBufferHeight, // full top
				cssPosition.x * pixelRatio | 0, // rect x
				cssPosition.y * pixelRatio | 0, // rect y
				1, // rect width
				1, // rect height
			);
			// render the scene
			renderer.setRenderTarget( pickingTexture );
			renderer.render( scene, camera );
			renderer.setRenderTarget( null );
			// clear the view offset so rendering returns to normal
			camera.clearViewOffset();
			//read the pixel
			renderer.readRenderTargetPixels(
				pickingTexture,
				0, // x
				0, // y
				1, // width
				1, // height
				pixelBuffer );

			const id =
          ( pixelBuffer[ 0 ] << 0 ) |
          ( pixelBuffer[ 1 ] << 8 ) |
          ( pixelBuffer[ 2 ] << 16 );

			return id;

		}

	}

	const pickHelper = new GPUPickHelper();

	function getCanvasRelativePosition( event ) {

		const rect = canvas.getBoundingClientRect();
		return {
			x: ( event.clientX - rect.left ) * canvas.width / rect.width,
			y: ( event.clientY - rect.top ) * canvas.height / rect.height,
		};

	}

	function pickCountry( event ) {

		// exit if we have not loaded the data yet
		if ( ! countryInfos ) {

			return;

		}

		const position = getCanvasRelativePosition( event );
		const id = pickHelper.pick( position, pickingScene, camera );
		if ( id > 0 ) {

			const countryInfo = countryInfos[ id - 1 ];
			const selected = ! countryInfo.selected;
			if ( selected && ! event.shiftKey && ! event.ctrlKey && ! event.metaKey ) {

				unselectAllCountries();

			}

			numCountriesSelected += selected ? 1 : - 1;
			countryInfo.selected = selected;

		} else if ( numCountriesSelected ) {

			unselectAllCountries();

		}

		requestRenderIfNotRequested();

	}

	function unselectAllCountries() {

		numCountriesSelected = 0;
		countryInfos.forEach( ( countryInfo ) => {

			countryInfo.selected = false;

		} );

	}

	canvas.addEventListener( 'pointerup', pickCountry );

	function resizeRendererToDisplaySize( renderer ) {

		const canvas = renderer.domElement;
		const width = canvas.clientWidth;
		const height = canvas.clientHeight;
		const needResize = canvas.width !== width || canvas.height !== height;
		if ( needResize ) {

			renderer.setSize( width, height, false );

		}

		return needResize;

	}

	let renderRequested = false;

	function render() {

		renderRequested = undefined;

		if ( resizeRendererToDisplaySize( renderer ) ) {

			const canvas = renderer.domElement;
			camera.aspect = canvas.clientWidth / canvas.clientHeight;
			camera.updateProjectionMatrix();

		}

		controls.update();

		updateLabels();

		renderer.render( scene, camera );

	}

	render();

	function requestRenderIfNotRequested() {

		if ( ! renderRequested ) {

			renderRequested = true;
			requestAnimationFrame( render );

		}

	}

	controls.addEventListener( 'change', requestRenderIfNotRequested );
	window.addEventListener( 'resize', requestRenderIfNotRequested );

}

main();
</script>
</html>
